package panda.client.listeners;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Set;

import panda.client.Cardpanda;
import panda.client.engine.MagicCardToken;
import panda.client.ui.SnapBorder;
import panda.client.ui.widgets.Bin;
import panda.client.ui.widgets.Counter;
import panda.client.ui.widgets.DraggableCard;
import panda.client.ui.widgets.NoteTip;
import panda.client.ui.widgets.Tooltip;

import panda.client.JSNI;

import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.DeferredCommand;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.ui.MouseListener;
import com.google.gwt.user.client.ui.Widget;

/**
 * Allows a DraggableCard to be dragged around the canvas. Manages snap positions and snapping.
 * 
 * @author zane.tian@gmail.com
 */
public class CardDragListener implements MouseListener {
	private int xStart; /* position in source widget where mouse is before dragging */
	private int yStart;

	private int xLast; /* position of x coordinate before the last move (Panda.canvas.setWidgetPosition) */
	private int yLast;
	private boolean tappedWhenMouseDown;

	/**
	 * SnapBorder coordinates within snaps are all with respect to the canvas. This means a card's x coordinate is not at
	 * card.getAbsLeft, but rather card.getAbsLeft - Panda.canvas.getAbsLeft.
	 */
	private static HashMap<String, SnapBorder> snaps = new HashMap<String, SnapBorder>();
	private static HashMap<String, DraggableCard> initialCardOverlap = new HashMap<String, DraggableCard>();
	private static List<Counter> initialCounterOverlap = new ArrayList<Counter>();

	private static final int xSnapThreshold = 10;
	private static final int ySnapThreshold = 10;

	private long timeOfLastMouseDown = 0;
	private static final int clickToDragThreshold = 125;
	
	private final String name = Cardpanda.clientModel.getSessionID().getName();

	private Timer dragInitializeTimer;
	
	public void onMouseDown(Widget src, final int x, final int y) {
		final DraggableCard card = (DraggableCard) src;
		DOM.eventPreventDefault(DOM.eventGetCurrentEvent());
		
		dragInitializeTimer = new Timer() {
			@Override
			public void run() {
				tappedWhenMouseDown = card.isTapped();

				if(tappedWhenMouseDown) {
					card.untap();
				}

				card.addStyleName("mouseStyle-dragging");

				DOM.setCapture(card.getElement());
				card.setDragging(true);
				xStart = x;
				yStart = y;

				if(tappedWhenMouseDown) {
					xStart = x - card.getTapOffset();
					yStart = y + card.getTapOffset();
				}

				/* snap test area */
				removeSnaps(card);

				initialCardOverlap.clear();
				initialCounterOverlap.clear();

				/* create a map of what other cards initially overlapped with this one */
				if(PandaKeyboardListener.holdingShift) {
					Set<String> snapKeys = snaps.keySet();
					for(String key : snapKeys) {
						int left = snaps.get(key).getxLeft();
						int top = snaps.get(key).getyTop();
						int draggingCardLeft = card.getAbsoluteLeft() - Cardpanda.canvas.getAbsoluteLeft();
						int draggingCardTop = card.getAbsoluteTop() - Cardpanda.canvas.getAbsoluteTop();

						int overlapArea = getOverlapArea(draggingCardLeft, draggingCardTop, left, top, card);
						if(overlapArea > 0) {
							/* put the card in the initialOverlap map if any overlap is found */
							initialCardOverlap.put(key, snaps.get(key).getCard());
						}
					}

					/* remove the snaps of the cards we're dragging along so we don't snap to their old positions */
					for(String key : initialCardOverlap.keySet()) {
						removeSnaps(initialCardOverlap.get(key));
					}
					
					/* add the dragging CSS style to each overlapping card since they will be dragged too */
					for(String key : initialCardOverlap.keySet()) {
						DraggableCard overlappingDragCard = initialCardOverlap.get(key);
						overlappingDragCard.addStyleName("mouseStyle-dragging");
					}
				}
				
				/* create a map of counters that overlap with this one. this is default behavior without having to hold shift */
				int overlappingCounters = 0;
				for(Counter counter : Cardpanda.clientModel.getCounters()) {
					if(counterOverlapsWithCard(counter, card)) {
						overlappingCounters++;
						initialCounterOverlap.add(counter);
					}
				}
			}
		};
		
		if(Cardpanda.CLICK_TO_TAP) {
			dragInitializeTimer.schedule((int) clickToDragThreshold);
			timeOfLastMouseDown = getCurrentTime();
		} else {
			dragInitializeTimer.run();
		}
	}

	/**
	 * Places a card's snapping coordinates (left and top borders) into the static snapMap in DragListener. If a card is
	 * tapped, then it places coordinates as if the card were not tapped.<br>
	 * Coordinates are adjusted to reflect actual position within Panda.canvas, not just card.absoluteCoordinateValue.
	 * 
	 * @param card
	 */
	public static void placeSnaps(DraggableCard card) {
		String key = String.valueOf(card.getMagicCard().getNameAndServerID());

		int snapx;
		int snapy;
		if(card.isTapped()) {
			snapx = card.getAbsoluteLeft() + card.getTapOffset() - Cardpanda.canvas.getAbsoluteLeft();
			snapy = card.getAbsoluteTop() - card.getTapOffset() - Cardpanda.canvas.getAbsoluteTop();
		} else {
			snapx = card.getAbsoluteLeft() - Cardpanda.canvas.getAbsoluteLeft();
			snapy = card.getAbsoluteTop() - Cardpanda.canvas.getAbsoluteTop();
		}

		// System.out.println("[c] carddraglistener.placesnaps - " + card.getName() + " placed at (" + snapx + ", " +
		// snapy + ")");
		snaps.put(key, new SnapBorder(card, snapx, snapy));
	}

	/**
	 * Removes the snap coordinates of a target card.
	 * 
	 * @param card
	 */
	public static void removeSnaps(DraggableCard card) {
		String key = String.valueOf(card.getMagicCard().getNameAndServerID());
		snaps.remove(key);
	}

	/** holy cow this is horrible code. must have been around 5am TODO: clean up this shit */
	private boolean counterOverlapsWithCard(Counter counter, DraggableCard card) {
		boolean overlapsX = false;
		int counterXstart = counter.getAbsoluteLeft();
		int counterXend = counterXstart + Counter.width;
		int cardXstart = card.getAbsoluteLeft();
		int cardXend = cardXstart + card.getOffsetWidth();

		if(between(counterXstart, cardXstart, cardXend) || between(counterXend, cardXstart, cardXend)) {
			overlapsX = true;
		}

		boolean overlapsY = false;
		int counterYstart = counter.getAbsoluteTop();
		int counterYend = counterYstart + Counter.height;
		int cardYstart = card.getAbsoluteTop();
		int cardYend = cardYstart + card.getOffsetHeight();

		if(between(counterYstart, cardYstart, cardYend) || between(counterYend, cardYstart, cardYend)) {
			overlapsY = true;
		}

		return overlapsX && overlapsY;
	}

	/**
	 * Checks to see if a point p is between two other points, start and end.
	 */
	private boolean between(int p, int start, int end) {
		if(end < start) {
			throw new IllegalArgumentException("endpoint needs to be >= startpoint");
		}
		return(p >= start && p <= end);
	}

	/**
	 * Determines overlap area of 2 congruent rectangles.
	 * 
	 * @return
	 */
	private int getOverlapArea(int x1, int y1, int x2, int y2, DraggableCard exampleCard) {
		int cardWidth = exampleCard.getOffsetWidth();
		int cardHeight = exampleCard.getOffsetHeight();

		int xOverlap = getOverlapLength(x1, x2, cardWidth);
		int yOverlap = getOverlapLength(y1, y2, cardHeight);

		return xOverlap * yOverlap;
	}

	/**
	 * Determines overlap length of 2 parallel line segments of the same length.
	 * 
	 * @return
	 */
	private int getOverlapLength(int p1, int p2, int lineLength) {
		/*
		 * There are 4 cases total where p1 is the starting coordinate of the first card and p2 is the starting coordinate
		 * of the second card. For reference, the length of p1 is p1 + lineLength.
		 */
		final int result;

		if(p1 >= p2 + lineLength) { /* p2 starts and ends before p1 begins */
			result = 0;
		} else if(p1 + lineLength <= p2) { /* p1 starts and ends before p2 begins */
			result = 0;
		} else if(p2 >= p1 && p2 <= p1 + lineLength) { /* p2 starts between p1's start and end coordinates */
			result = p1 + lineLength - p2;
		} else if(p1 >= p2 && p1 <= p2 + lineLength) { /* p1 starts between p2's start and end coordinates */
			result = p2 + lineLength - p1;
		} else
			throw new IllegalStateException("Not in one of the 4 cases in getOverLapLength D:");

		return result;
	}

	/* setBinHover if this card is dragged over a bin */
	private void setBinHover(DraggableCard card, int x, int y) {
		final List<Bin> bins = Cardpanda.clientModel.getBinsThisClientControls();
		for(Bin bin : bins) {
			if(withinBinCoordinates(card, bin, x, y)) {
				bin.setHover();
			} else {
				bin.removeHover();
			}
		}
	}

	public void onMouseMove(Widget src, int x, int y) {
		DOM.eventPreventDefault(DOM.eventGetCurrentEvent());
		DraggableCard card = (DraggableCard) src;
		int xAbs = x + card.getAbsoluteLeft() - Cardpanda.canvas.getAbsoluteLeft();
		int yAbs = y + card.getAbsoluteTop() - Cardpanda.canvas.getAbsoluteTop();

		/**
		 * Snapping code. <br>
		 * <br>
		 * The main idea behind snapping is to provide convenience that doesn't get in the way.
		 */
		if(card.isDragging()) {
			setBinHover(card, x, y); /* if this card is over a bin, let the bin set its hover style */

			moveCard(card, xAbs - xStart, yAbs - yStart);
			/**
			 * How many cards overlap with the dragging card? <br>
			 * The overlapping cards, if any, have their names and IDs put into overlapMap<String serverID, Integer
			 * overlapArea>. Seems to work reliably.
			 */
			HashMap<String, Integer> overlapMap = new HashMap<String, Integer>();

			Set<String> snapKeys = snaps.keySet();
			for(String key : snapKeys) {
				int left = snaps.get(key).getxLeft();
				int top = snaps.get(key).getyTop();

				/* draggingCardLeft is equivalent to card.getAbsLeft + x - xStart */
				int draggingCardLeft = xAbs - xStart/* + Panda.canvas.getAbsoluteLeft() */;
				int draggingCardTop = yAbs - yStart/* + Panda.canvas.getAbsoluteTop() */;

				int overlapArea = getOverlapArea(draggingCardLeft, draggingCardTop, left, top, card);
				if(overlapArea > 0) {
					overlapMap.put(key, overlapArea);
					// System.out.println("[c] carddraglistener.onmousemove: overlapMap adds " + key);
				}
			}

			/* Which card overlaps the most with the dragging card? Seems to work reliably. */
			int max = 0;
			String nameOfMaxOverlapCard = null;
			for(String key : overlapMap.keySet()) {
				if(overlapMap.get(key) > max) {
					max = overlapMap.get(key);
					nameOfMaxOverlapCard = key;
				}
			}

			/* no cards overlap = only snap y */
			if(overlapMap.isEmpty()) {

				/* search through snaps to see if any are in range. if so, snap! */
				for(String key : snaps.keySet()) {
					int snapY = snaps.get(key).getyTop();
					int yDist = Math.abs(card.getAbsoluteTop() - Cardpanda.canvas.getAbsoluteTop() - snapY);
					boolean yInSnapRange = yDist < ySnapThreshold;

					if(yInSnapRange) {
						moveCard(card, xAbs - xStart, snapY/* - Panda.canvas.getAbsoluteTop() */);
						break; /* so snaps listed first don't override other snaps! */
					} else {
						/* not in range */
					}
				}
			} else {
				/**
				 * the dragged card does overlap with at least 1 other card: prepare to snap both x and y to the card with
				 * the most overlap
				 * 
				 * @author vickyl
				 */
				int top = snaps.get(nameOfMaxOverlapCard).getyTop();
				int left = snaps.get(nameOfMaxOverlapCard).getxLeft();
				int yDist = Math.abs(card.getAbsoluteTop() - Cardpanda.canvas.getAbsoluteTop() - top);
				int xDist = Math.abs(card.getAbsoluteLeft() - Cardpanda.canvas.getAbsoluteLeft() - left);
				boolean yInSnapRange = yDist < ySnapThreshold;
				boolean xInSnapRange = xDist < xSnapThreshold;

				/**
				 * For clarity, compare the following arguments to the default nonsnapping moveCard call:<br>
				 * moveCard(card, xAbs - xStart, yAbs - yStart);
				 */
				if(xInSnapRange && yInSnapRange) {
					moveCard(card, left, top);
				} else if(yInSnapRange) {
					moveCard(card, xAbs - xStart, top);
				} else if(xInSnapRange) {
					moveCard(card, left, yAbs - yStart);
				}
			}
		}
	}

	/**
	 * Move this card to its specified position on the canvas. By calculating how much it moved since the last moveCard
	 * call, we can correctly position all the cards and counters that are being SHIFT-dragged with this card, if there
	 * are any.
	 */
	private void moveCard(DraggableCard card, int xPos, int yPos) {
		/**
		 * This is how much this card moved since the last time moveCard was called. We'll use this result to make all the
		 * cards and counters SHIFT-dragging with this card move the same distance.
		 */
		xLast = card.getAbsoluteLeft();
		yLast = card.getAbsoluteTop();
		int xChange = xPos - xLast + Cardpanda.canvas.getAbsoluteLeft();
		int yChange = yPos - yLast + Cardpanda.canvas.getAbsoluteTop();

		/* the card's position needs to be set AFTER xLast and yLast are set */
		Cardpanda.canvas.setWidgetPosition(card, xPos, yPos);

		/* move all the overlapping cards that are SHIFT-dragging with this card */
		for(String key : initialCardOverlap.keySet()) {
			if(key.equals(card.getMagicCard().getNameAndServerID())) {
				continue;
			}
			int oldX = initialCardOverlap.get(key).getAbsoluteLeft() - Cardpanda.canvas.getAbsoluteLeft();
			int oldY = initialCardOverlap.get(key).getAbsoluteTop() - Cardpanda.canvas.getAbsoluteTop();

			int newX = oldX + xChange;
			int newY = oldY + yChange;
			Cardpanda.canvas.setWidgetPosition(initialCardOverlap.get(key), newX, newY);
		}

		/* move all the overlapping counters that are SHIFT-dragging with this card */
		for(Counter c : initialCounterOverlap) {
			int oldX = c.getAbsoluteLeft() - Cardpanda.canvas.getAbsoluteLeft();
			int oldY = c.getAbsoluteTop() - Cardpanda.canvas.getAbsoluteTop();

			int newX = oldX + xChange;
			int newY = oldY + yChange;
			Cardpanda.canvas.setWidgetPosition(c, newX, newY);
		}
	}

	// private long getTimeSinceLastMouseUp() {
	// return getCurrentTime() - timeOfLastMouseUp;
	// }
	//	
	// private long getCurrentTime() {
	// return (long) JSNI.getCurrentMs();
	// }

	/**
	 * Untap the card if it was tapped before dragging.<br>
	 * Remove the drag style for this card and all other dragged cards.<br>
	 * Place the snap coordinates for this card and any cards dragging with it.<br>
	 * If this card is over a bin, put it into the bin (if the dragTarget is a token, just remove it from the game)<br>
	 * Update the server.
	 */
	public void onMouseUp(Widget src, int x, int y) {
		final DraggableCard card = (DraggableCard) src;

		final long timeSinceLastMouseDown = getCurrentTime() - timeOfLastMouseDown;
		System.out.println("time since last mouseup " + timeSinceLastMouseDown);
		
		if(Cardpanda.CLICK_TO_TAP && timeSinceLastMouseDown < clickToDragThreshold) {
			dragInitializeTimer.cancel();
			Cardpanda.clientModel.tapToggle(card);
			Cardpanda.sendModelUpdateToServer(Cardpanda.clientModel.getSessionID());
		} else {
			/**
			 * Check if this card is hovering over any bins. If so, perform the appropriate action:<br>
			 * 1. remove from game for tokens<br>
			 * 2. transfer to a bin for cards<br>
			 * After this, clear lastMousedOverCard so we don't try to zoom (actually works fine) or tap (not ok) We also
			 * should assert that we are only hovering over one bin :P
			 */
			List<Bin> bins = Cardpanda.clientModel.getBinsThisClientControls();
			Bin targetBin = null;
			int numBinsUnderCard = 0; /* number of bins this card is hovering over */
			boolean binUnderCardFound = false;

			for(Bin bin : bins) {
				if(withinBinCoordinates(card, bin, x, y)) {
					numBinsUnderCard++;
					binUnderCardFound = true;
					targetBin = bin;
				}
			}

			assert numBinsUnderCard < 2 : "There shouldn't be more than 1 bin this card is hovering over";

			if(binUnderCardFound) {
				String binDetail = targetBin.getOwnerAndType();

				if(card.getMagicCard() instanceof MagicCardToken) {
					/* for tokens, remove the token from play. */
					Cardpanda.clientModel.removeCardInPlay(card);

					String end = ". Because it is a token, it is removed from the game.";
					String logForMe = "You put " + card.getName() + " on top of " + binDetail + end;
					String logForOthers = name + " puts " + card.getName() + " on top of " + binDetail + end;
					NoteTip.get().log(logForMe, logForOthers);
				} else {
					/* for other draggablecards, move the card into target bin */
					Cardpanda.clientModel.removeCardInPlay(card);
					targetBin.addCardToTop(card);
					Cardpanda.clientModel.setLastMousedOverCard(null);

					String logForMe = "You put " + card.getName() + " on top of " + binDetail + ".";
					String logForOthers = name + " puts " + card.getName() + " on top of " + binDetail + ".";
					NoteTip.get().log(logForMe, logForOthers);
				}

				DeferredCommand.addCommand(new Command() {
					public void execute() {
						Tooltip.get().hide(); /* counters the showTooltip method in tooltipListener's onmouseup */
					}
				});
				targetBin.removeHover();

				/* set lastMousedOverCard to null */
				Cardpanda.clientModel.setLastMousedOverCard(null);

			} else { /* this card isn't hovering over any bins */
				final int cardx = card.getAbsoluteLeft() - Cardpanda.canvas.getAbsoluteLeft();
				final int cardy = card.getAbsoluteTop() - Cardpanda.canvas.getAbsoluteTop();

				String logForMe = "You move " + card.getName() + " to " + "(" + cardx + ", " + cardy + ").";
				String logForOthers = Cardpanda.clientModel.getSessionID().getName() + " moves " + card.getName() + " to " + "("
						+ cardx + ", " + cardy + ").";
				NoteTip.get().log(logForMe, logForOthers);
			}

			/* The order of this call is important. See withinBinCoordinates */
			if(!binUnderCardFound && tappedWhenMouseDown) {
				card.tap();
			}

			card.removeStyleName("mouseStyle-dragging");
			
			/* remove the dragging CSS style to each overlapping card since they will be dragged too */
			for(String key : initialCardOverlap.keySet()) {
				DraggableCard overlappingDragCard = initialCardOverlap.get(key);
				
				overlappingDragCard.removeStyleName("mouseStyle-dragging");
			}

			DOM.releaseCapture(card.getElement());
			card.setDragging(false);

			/* place the snaps of the card that was dragged */
			placeSnaps(card);

			/* need to place any cards that have been moved via overlap too */
			for(String key : initialCardOverlap.keySet()) {
				if(key.equals(card.getMagicCard().getNameAndServerID())) {
					continue;
				}
				placeSnaps(initialCardOverlap.get(key));
			}

			Cardpanda.sendModelUpdateToServer(Cardpanda.clientModel.getSessionID());
		}		
		
		
	}

	/** Returns true if the mouse is positioned over target bin. */
	private boolean withinBinCoordinates(DraggableCard card, Bin bin, int x, int y) {
		/* Bounds = absolute coordinates of mouse cursor */
		int mouseX;
		int mouseY;

		mouseX = card.getAbsoluteLeft() - Cardpanda.canvas.getAbsoluteLeft() + x;
		mouseY = card.getAbsoluteTop() - Cardpanda.canvas.getAbsoluteTop() + y;

		/**
		 * Tapped cards are untapped temporarily when dragged, but untap when onMouseUp is fired. In onMouseUp, they untap
		 * BEFORE the last withinBinCoordinates is fired, so when that check is done we want to assume an untapped card.
		 * No card should ever be tapped when dragging so this special case below only applies to the specific order of
		 * code in onMouseUp. Should be fixed in a cleaner manner though :|<br>
		 * <br>
		 * Ok so I fixed it :D The card should never be tapped at this point.
		 */
		assert !card.isTapped() : "Cards should not be tapped while checking for bin collision";
		// if(card.isTapped()) {
		// mouseX = card.getAbsoluteLeft() - Panda.canvas.getAbsoluteLeft() + x + card.getTapOffset();
		// mouseY = card.getAbsoluteTop() - Panda.canvas.getAbsoluteTop() + y - card.getTapOffset();
		// } else {
		// mouseX = card.getAbsoluteLeft() - Panda.canvas.getAbsoluteLeft() + x;
		// mouseY = card.getAbsoluteTop() - Panda.canvas.getAbsoluteTop() + y;
		// }
		//
		// System.out.println(mouseX + "," + mouseY);

		/* The bin's 4 corners lie on xLeft xRight yTop yBottom. */
		/* Note: these absolute positions rely on the canvas's absolute left and top as well. */
		int xLeft = bin.getLabel().getAbsoluteLeft() - Cardpanda.canvas.getAbsoluteLeft();
		int yTop = bin.getLabel().getAbsoluteTop() - Cardpanda.canvas.getAbsoluteTop();
		int yBottom = bin.getLabel().getAbsoluteTop() + bin.getHeight() - Cardpanda.canvas.getAbsoluteTop();
		int xRight = bin.getLabel().getAbsoluteLeft() + bin.getWidth() - Cardpanda.canvas.getAbsoluteLeft();

		// System.out.println("[c] carddraglistener.withinBinCoordinates for " + bin.getOwnerAndType() + ": "
		// + (mouseX >= xLeft && mouseX <= xRight && mouseY >= yTop && mouseY <= yBottom));
		return mouseX >= xLeft && mouseX <= xRight && mouseY >= yTop && mouseY <= yBottom;
	}

	public void onMouseEnter(Widget src) {
		src.addStyleName("mouseStyle-mouseover");

		DraggableCard card = (DraggableCard) src;

		// System.out.println("[c] cdraglistener.onmouseenter - looking at: " + card.getImgUrl());
		Cardpanda.clientModel.setLastMousedOverCard(card);
		Cardpanda.focusPnl.setFocus(true);
	}

	public void onMouseLeave(Widget src) {
		src.removeStyleName("mouseStyle-mouseover");
	}

	public long getCurrentTime() {
		return (long) JSNI.getCurrentMs();
	}
}